<template>
  <div class="cg-image-edit" :class="{ 'is-disabled': disabled }" ref="cg_image_edit">
    <div
      class="img-preview"
      @mouseenter="previewHovering = true"
      @mouseleave="previewHovering = false"
    >
      <canvas ref="clipShow" :width="showClipSize.with" :height="showClipSize.height"></canvas>
      <input
        ref="file_input"
        class="upload-instance"
        type="file"
        accept="image/png,image/jpg,image/jpeg"
        @change="handleChange"
      />
      <div class="img-upload-text" v-show="previewHovering || disabled" @click="handleClick">
        <template v-if="!disabled">
          <span v-show="imgData">重新上传图片</span>
          <span v-show="!imgData">点击上传图片</span>
        </template>
      </div>
    </div>
    <div class="img-clip" v-show="selected && !disabled" :style="clipStyle">
      <div class="img-clip-text">裁剪区域</div>
      <div class="img-container">
        <div class="img-wrap" :style="boxStyle">
          <div ref="img_clip_box" class="img-clip-box" :style="imageStyle">
            <img :src="imgData" draggable="false" :style="imageStyle" ref="img" />
            <div class="shadow-box"></div>

            <div class="crop-wrap">
              <div class="crop-shadow-box" :style="cropBoxStyle">
                <img :src="imgData" draggable="false" :style="cropBoxImageStyle" />
              </div>
              <div ref="crop_box" class="crop-box" :class="{ show: cropShow }" :style="cropStyle">
                <span class="drag-point point-lt" drag="point" drag-data="lt" />
                <span class="drag-point point-lb" drag="point" drag-data="lb" />
                <span class="drag-point point-rt" drag="point" drag-data="rt" />
                <span class="drag-point point-rb" drag="point" drag-data="rb" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import _ from 'lodash'
import Hammer, { HammerManager } from 'hammerjs'
import { rafThrottle, limitValue, getRelativeOffset, isInitNumber } from '../../tools'

// 定义组件名称
defineOptions({
  name: 'cg-image-edit'
})

// 定义props
const props = withDefaults(
  defineProps<{
    modelValue: string | undefined
    ratio: string | undefined
  }>(),
  {}
)

// 定义emits
const emit = defineEmits<{
  (e: 'update:modelValue', value: string | undefined): void
}>()

// 组件引用
const clipShow = ref<HTMLCanvasElement | null>(null)
const file_input = ref<HTMLInputElement | null>(null)
const img_clip_box = ref<HTMLDivElement | null>(null)
const crop_box = ref<HTMLDivElement | null>(null)
const img = ref<HTMLImageElement | null>(null)
const cg_image_edit = ref<HTMLDivElement | null>(null)

// 响应式数据
const minClipWidth = ref(10) // 最小裁剪宽度
const clipSize = ref(800)
const rectSize = ref(400)
const imgData = ref<string | undefined>(undefined)
const selected = ref(false)
const cropShow = ref(true)
// 图片原分辨率
const imageWidth = ref(0)
const imageHeight = ref(0)

// 截取区域显示图片分辨率
const showWidth = ref(0)
const showHeight = ref(0)

// 拖曳位置，拖拽图片分辨率
const pointX = ref(0)
const pointY = ref(0)
const clipWidth = ref(100)
const clipHeight = ref(100)
const gesture = ref<HammerManager | undefined>(undefined)

const previewHovering = ref(false)

// 计算属性
const disabled = computed(() => {
  return !props.ratio
})

const clipStyle = computed(() => {
  return {
    width: rectSize.value + 'px'
  }
})

// 截取后展示的图片分辨率
const ratioCalc = computed(() => {
  const sep = ':'
  if (typeof props.ratio === 'string') {
    const sp = props.ratio.split(sep) // 宽高比9:16
    return _.toNumber(sp[0]) / _.toNumber(sp[1])
  }
  return undefined
})

const ratioValue = computed(() => {
  if (isInitNumber(ratioCalc.value)) {
    return ratioCalc.value
  }
  return 1
})

const showClipSize = computed(() => {
  return {
    width: clipSize.value,
    height: clipSize.value * ratioValue.value
  }
})

const boxStyle = computed(() => {
  return {
    height: rectSize.value + 'px',
    width: rectSize.value + 'px',
    paddingLeft: (rectSize.value - showWidth.value) / 2 + 'px',
    paddingTop: (rectSize.value - showHeight.value) / 2 + 'px'
  }
})

const imageStyle = computed(() => {
  return {
    width: showWidth.value + 'px',
    height: showHeight.value + 'px'
  }
})

const cropBoxImageStyle = computed(() => {
  return {
    ...imageStyle.value,
    left: -pointX.value + 'px',
    top: -pointY.value + 'px'
  }
})

const cropBoxStyle = computed(() => {
  return {
    height: clipHeight.value + 'px',
    width: clipWidth.value + 'px',
    left: pointX.value + 'px',
    top: pointY.value + 'px'
  }
})

const cropStyle = computed(() => {
  return {
    height: clipHeight.value + 'px',
    width: clipWidth.value + 'px',
    left: pointX.value + 'px',
    top: pointY.value + 'px'
  }
})

// 监听modelValue变化
watch(
  () => props.modelValue,
  () => {
    nextTick(() => {
      if (imgData.value) {
        return
      }
      updateModelValue()
    })
  }
)

// 生命周期钩子
onMounted(() => {
  updateModelValue()
  bindEvent()
})

onUnmounted(() => {
  unbindEvent()
})

// 方法
function bindEvent() {
  const dom = img_clip_box.value
  const crop = crop_box.value
  const el = cg_image_edit.value || document.body
  if (!dom || !crop) return

  const drag: Array<{ tag: string; dom: Element }> = []
  const list = crop.querySelectorAll('[drag=point]')
  list.forEach((el) => {
    const dragData = el.getAttribute('drag-data')
    if (dragData) {
      drag.push({
        tag: dragData,
        dom: el
      })
    }
  })

  const inDomRect = (dom: Element, event: HammerInput) => {
    const {
      center: { x: pointX, y: pointY }
    } = event
    const rect = dom.getBoundingClientRect()
    return rect.left < pointX && rect.right > pointX && rect.top < pointY && rect.bottom > pointY
  }

  const inDrag = (event: HammerInput) => {
    return drag.find((config) => {
      return inDomRect(config.dom, event)
    })
  }

  const hammer = new Hammer(dom)
  gesture.value = hammer
  hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 0 })

  const getRelativePoint = (event: HammerInput, dom: Element) => {
    const {
      center: { x: pointX, y: pointY }
    } = event
    const offset = getRelativeOffset(dom)
    const originX = pointX - offset.left
    const originY = pointY - offset.top
    return [originX, originY] as [number, number]
  }

  const offset = (left: number[], right: number[]) => {
    return [left[0] - right[0], left[1] - right[1]]
  }

  const add = (left: number[], right: number[]) => {
    return [left[0] + right[0], left[1] + right[1]]
  }

  hammer.on('panstart', (event) => {
    if (!selected.value) {
      return
    }
    let dragType = -1
    let dragTag = ''

    if (!cropShow.value) {
      const point = getRelativePoint(event, dom)
      cropShow.value = true
      pointX.value = point[0]
      pointY.value = point[1]
      dragType = 1
      dragTag = 'rb'
    } else {
      const inDom = inDomRect(crop, event)
      if (inDom) {
        dragType = 0
      } else {
        const dragRes = inDrag(event)
        if (dragRes) {
          dragType = 1
          dragTag = dragRes.tag
        }
      }
    }

    if (dragType === -1) {
      return
    }
    const sPoint = getRelativePoint(event, el)
    // 裁剪框的左上节点
    const sFix = [pointX.value, pointY.value] // 拖拽坐标，左上角顶点
    // 裁剪框大小
    const sClip = [clipWidth.value, clipHeight.value] // 拖拽窗体宽高
    // 裁剪框的右下节点
    const sRBFix = add(sFix, sClip)
    const rightBorder = showWidth.value - clipWidth.value
    const bottomBorder = showHeight.value - clipHeight.value
    const handleRuning = rafThrottle((ev) => {
      const rPoint = getRelativePoint(ev, el)
      const delta = offset(rPoint, sPoint)
      const eFix = add(sFix, delta)
      const eRBFix = add(eFix, sClip)
      if (dragType === 0) {
        // 直接平移
        pointX.value = limitValue(eFix[0], 0, rightBorder)
        pointY.value = limitValue(eFix[1], 0, bottomBorder)
      }
      if (dragType === 1) {
        // 点边缩放
        const vTag = dragTag[0]
        const hTag = dragTag[1]

        if (vTag === 'l') {
          let rRod = sRBFix[0]
          pointX.value = limitValue(eFix[0], 0, rRod)
          if (rRod <= pointX.value) {
            // 右侧
            clipWidth.value = pointX.value - rRod
          } else {
            // 左侧
            clipWidth.value = rRod - pointX.value
          }
          // 限制最小宽度
          clipWidth.value = Math.max(clipWidth.value, minClipWidth.value)
          // 确保右边缘不超出图片宽度
          clipWidth.value = Math.min(clipWidth.value, showWidth.value - pointX.value)
        }

        if (vTag === 'r') {
          let lRod = sFix[0]
          pointX.value = Math.min(lRod, eRBFix[0])
          if (lRod <= eRBFix[0]) {
            clipWidth.value = eRBFix[0] - lRod
          } else {
            clipWidth.value = lRod - eRBFix[0]
          }
          // 限制最小宽度
          clipWidth.value = Math.max(clipWidth.value, minClipWidth.value)
          // 确保右边缘不超出图片宽度
          clipWidth.value = Math.min(clipWidth.value, showWidth.value - pointX.value)
        }

        // 根据宽度计算高度后，反向限制高度不超过图片显示高度，并调整宽度保持比例
        clipHeight.value = clipWidth.value * ratioValue.value
        // 限制高度不超过图片显示高度
        clipHeight.value = Math.min(clipHeight.value, showHeight.value)
        // 反向调整宽度以保持比例，同时限制宽度不超过图片显示宽度
        clipWidth.value = Math.min(clipHeight.value / ratioValue.value, showWidth.value)
        // 确保宽度不小于最小限制
        clipWidth.value = Math.max(clipWidth.value, minClipWidth.value)
        // 重新计算高度保证比例一致性
        clipHeight.value = clipWidth.value * ratioValue.value
        if (hTag === 't') {
          let bRod = sRBFix[1]
          if (bRod <= eFix[1]) {
            pointY.value = bRod
          } else {
            pointY.value = bRod - clipHeight.value
          }
          // 限制最小高度（与宽度最小限制同步）
          clipHeight.value = Math.max(clipHeight.value, minClipWidth.value * ratioValue.value)
          // 确保底部不超出图片高度
          clipHeight.value = Math.min(clipHeight.value, showHeight.value - pointY.value)
          // 调整顶部位置避免超出
          pointY.value = Math.max(pointY.value, 0)
        }

        if (hTag === 'b') {
          let tRod = sFix[1]
          if (tRod <= eRBFix[1]) {
            pointY.value = tRod
          } else {
            pointY.value = tRod - clipHeight.value
          }
          // 限制最小高度（与宽度最小限制同步）
          clipHeight.value = Math.max(clipHeight.value, minClipWidth.value * ratioValue.value)
          // 确保底部不超出图片高度
          clipHeight.value = Math.min(clipHeight.value, showHeight.value - pointY.value)
          // 调整顶部位置避免超出
          pointY.value = Math.max(pointY.value, 0)
        }
      }
    })
    hammer.on('panmove', handleRuning)
    const handleEnd = () => {
      takePicture(true)
      hammer.off('panmove', handleRuning)
      hammer.off('panend', handleEnd)
      hammer.off('pancancel', handleEnd)
    }
    hammer.on('panend', handleEnd)
    hammer.on('pancancel', handleEnd)
  })
}

function unbindEvent() {
  if (gesture.value) {
    gesture.value.destroy()
    gesture.value = undefined
  }
}

function updateModelValue() {
  if (props.modelValue && isInitNumber(ratioCalc.value)) {
    updatePicData(props.modelValue)
  } else {
    updateEmptyPic()
  }
}

function handleClick() {
  if (disabled.value) {
    return
  }
  const dom = file_input.value
  if (dom) {
    dom.click()
  }
}

function handleChange(e: Event) {
  const target = e.target as HTMLInputElement
  const files = target.files
  if (!files) return
  uploadFiles(files)
}

function uploadFiles(files: FileList) {
  if (files.length) {
    const reader = new FileReader()
    reader.onload = (event) => {
      if (event.target?.result) {
        updatePicData(event.target.result as string, true)
      }
    }
    reader.readAsDataURL(files[0])
  }
}

function updateEmptyPic() {
  const dom = clipShow.value
  if (dom) {
    const ctx = dom.getContext('2d')
    if (ctx) {
      ctx.fillStyle = '#f2f2f2'
      ctx.fillRect(0, 0, dom.width, dom.height)
      ctx.globalAlpha = 1
      emit('update:modelValue', undefined)
    }
  }
}

function updatePicData(data: string, isInput = false) {
  imgData.value = data
  const imageDom = new Image()
  imageDom.src = data
  imageDom.onload = () => {
    imageWidth.value = imageDom.width
    imageHeight.value = imageDom.height
    const imgRadio = imageWidth.value / imageHeight.value

    if (imgRadio < 1) {
      showWidth.value = rectSize.value * imgRadio
      showHeight.value = showWidth.value / imgRadio
    } else {
      showHeight.value = rectSize.value / imgRadio
      showWidth.value = showHeight.value * imgRadio
    }
    if (isInput) {
      clipWidth.value = 100
      clipHeight.value = clipWidth.value * ratioValue.value
    }
    nextTick(() => {
      if (isInput) {
        selected.value = true
      }
      takePicture(isInput)
    })
  }
}

// 导出图片到剪切面板
function takePicture(isInput: boolean) {
  const dom = clipShow.value
  const imgDom = img.value
  if (dom && imgDom) {
    const ctx = dom.getContext('2d')
    if (ctx) {
      ctx.clearRect(0, 0, dom.width, dom.height)
      ctx.globalAlpha = 1

      let sx = 0,
        sy = 0,
        sWidth = 0,
        sHeight = 0
      if (isInput) {
        const vTo = (v: number) => (v / showWidth.value) * imageWidth.value
        const hTo = (h: number) => (h / showHeight.value) * imageHeight.value
        sx = vTo(pointX.value)
        sy = vTo(pointY.value)
        sWidth = vTo(clipWidth.value)
        sHeight = hTo(clipHeight.value)
      } else {
        let imageRatio = imageHeight.value / imageWidth.value
        if (imageRatio < ratioValue.value) {
          sWidth = imageHeight.value / ratioValue.value
          sHeight = imageHeight.value
        } else {
          sWidth = imageWidth.value
          sHeight = imageWidth.value * ratioValue.value
        }
      }

      ctx.drawImage(imgDom, sx, sy, sWidth, sHeight, 0, 0, dom.width, dom.height)
      emit('update:modelValue', dom.toDataURL('image/jpeg', 1))
    }
  }
}
</script>

<style lang="scss">
.cg-image-edit {
  white-space: nowrap;

  .img-clip {
    padding-top: 1rem;
    text-align: center;

    .img-clip-text {
      background-color: #e9ecef;
      padding: 0.75rem 1rem;
      border-radius: 0.25rem;
      font-size: 1.25rem;
    }
  }

  .img-preview {
    vertical-align: top;
    position: relative;
    display: inline-block;
    font-size: 0;

    canvas {
      width: 200px;
    }

    .img-upload-text {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      color: #fff;
      cursor: pointer;
      font-size: 1rem;
      text-align: center;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color: rgba(#000000, 0.5);
    }
  }

  &[disabled='true'] {
    .img-upload-text {
      cursor: not-allowed;
    }
  }

  .img-container {
    .img-wrap {
      background: #606266;
    }

    img {
      position: relative;
      width: 100%;
      height: 100%;
      user-select: none;
    }

    .img-clip-box {
      position: relative;
      margin: 0;
    }

    .shadow-box {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
      z-index: 1;
    }

    .crop-wrap {
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 10;
      cursor: crosshair;
    }

    .crop-shadow-box {
      overflow: hidden;
      z-index: 1;
      left: 0;
      top: 0;
      position: absolute;
    }

    .crop-box {
      position: absolute;
      display: none;
      left: 0px;
      top: 0px;
      width: 0;
      height: 0;
      cursor: move;
      border: 1px solid #fff;
      z-index: 2;
      box-sizing: border-box;

      &.show {
        display: block;
      }

      .point-lt {
        top: -10px;
        left: -10px;
        cursor: nw-resize;
      }

      .point-lb {
        left: -10px;
        bottom: -10px;
        cursor: sw-resize;
      }

      .point-rt {
        right: -10px;
        top: -10px;
        cursor: ne-resize;
      }

      .point-rb {
        right: -10px;
        bottom: -10px;
        cursor: se-resize;
      }
    }

    .drag-point {
      display: inline-block;
      width: 10px;
      height: 10px;
      border: 1px solid #fff;
      position: absolute;
      box-sizing: border-box;
    }
  }

  .upload-instance {
    display: none;
  }
}
</style>
